package com.portal.file.service.impl;


import com.portal.common.core.web.domain.AjaxResult;
import com.portal.common.redis.service.RedisService;
import com.portal.file.config.MinioConfig;
import com.portal.file.domain.upload.FileChunkDTO;
import com.portal.file.domain.upload.FileChunkResultDTO;
import com.portal.file.service.MinioService;
import com.portal.file.service.UploadService;
import com.portal.system.api.domain.SysFile;
import io.minio.ComposeSource;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Slf4j
@Service
public class UploadServiceImpl implements UploadService {
    @Resource
    private MinioConfig minioConfig;

    @Resource
    private MinioService minioService;

    @Resource
    private RedisService redisService;

    /**
     * @param fileName    文件名称
     * @param totalChunks 文件分片总数
     */
    public void delSliceFile(String fileName, Integer totalChunks) {
        // 删除所有的临时分片文件
        List<DeleteObject> delObjects = Stream.iterate(1, i -> ++i)
                .limit(totalChunks)
                .map(i -> new DeleteObject(fileName.concat("/").concat(Integer.toString(i))))
                .collect(Collectors.toList());
        Boolean isDel = minioService.removeFiles(minioConfig.getSliceBucket(), delObjects);
        if (isDel) {
            log.info("临时分片文件==>{}<===删除成功", fileName);
        }
    }

    /**
     * 检查文件是否存在，如果存在则跳过该文件的上传，如果不存在，返回需要上传的分片集合
     * 检查分片是否存在
     * ○ 检查目录下的文件是否存在。
     * ○ 检查redis存储的分片是否存在。
     * ○ 判断分片数量和总分片数量是否一致。
     * 如果文件存在并且分片上传完毕，标识已经完成附件的上传，可以进行秒传操作。
     * 如果文件不存在或者分片为上传完毕，则返回false并返回已经上传的分片信息。
     *
     * @param chunkDTO c
     * @return 结果
     */
    @Override
    public FileChunkResultDTO checkChunkExist(FileChunkDTO chunkDTO) {
        //1.检查文件是否已上传过
        //1.1)检查在磁盘中是否存在
        //1.2)检查Redis中是否存在,并且所有分片已经上传完成。
        Set<Integer> uploaded = redisService.getCacheMapValue(chunkDTO.getIdentifier(), "uploaded");
        if (uploaded != null && uploaded.size() == chunkDTO.getTotalChunks()) {
            return new FileChunkResultDTO(true);
        }
        // 断点续传，返回已上传的分片
        return new FileChunkResultDTO(false, uploaded);
    }

    @Override
    public Boolean uploadChunk(FileChunkDTO chunkDTO) {
        //写入分片到minio
        try {
            minioService.uploadFile(
                    minioConfig.getSliceBucket(),
                    chunkDTO.getFilename(),
                    chunkDTO.getChunkNumber(),
                    chunkDTO.getFile());
            log.info("文件标识:{},chunkNumber:{},currentChunkSize:{}", chunkDTO.getIdentifier(), chunkDTO.getChunkNumber(), chunkDTO.getCurrentChunkSize());
            //将该分片写入redis
            long size = saveToRedis(chunkDTO);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    @Override
    public AjaxResult mergeChunk(String identifier, String fileName, Integer totalChunks) {
        // 获取缓存目录中的数量
        Set<String> fileObjects = minioService.getFileObjects(minioConfig.getSliceBucket(), fileName);
        boolean checkChunks = checkChunks(fileObjects.size(), totalChunks);
        if (checkChunks) {
            Boolean flag = composeFile(identifier, fileName, totalChunks);
            if (flag) {
                // TODO 后期可以通过MQ调用：缓存1800s后才执行1、删除分片数据和删除Redis中的数据
                delSliceFile(fileName, totalChunks);
                redisService.expire(identifier,-1);
                return AjaxResult.success("合并成功");
            }
        }
        return AjaxResult.error("合并失败");
    }

    /**
     * 检查分片是否都存在
     *
     * @param minioChunks minio中已存在的个数
     * @param totalChunks 文件分片个数
     * @return true都存在，false 不全
     */
    private boolean checkChunks(Integer minioChunks, Integer totalChunks) {
        return minioChunks.equals(totalChunks);
    }

    /**
     * 分片文件合并
     *
     * @param identifier  文件名md5
     * @param totalChunks 总分片数量
     */
    public Boolean composeFile(String identifier, String fileName, Integer totalChunks) {
        // 完成上传从缓存目录合并迁移到正式目录
        List<ComposeSource> sourceObjectList = Stream.iterate(1, i -> ++i)
                .limit(totalChunks)
                .map(i -> ComposeSource.builder()
                        .bucket(minioConfig.getSliceBucket())
                        .object(fileName.concat("/").concat(Integer.toString(i)))
                        .build())
                .collect(Collectors.toList());
        log.debug("文件合并|composeFile|参数文件MD5:{},文件名称:{},总分片数:{}", identifier, fileName, totalChunks);
        // 合并操作
        Boolean isCompose = minioService.composeFile(minioConfig.getBucketName(), fileName, sourceObjectList);
        return isCompose;
    }

    /**
     * 初始化时创建桶
     *
     * @return Boolean ture成功，false是失败
     */
    public Boolean createFileBucket() {
        boolean fileBucket = minioService.createBucket(minioConfig.getBucketName());
        boolean sliceBucket = minioService.createBucket(minioConfig.getSliceBucket());
        if (sliceBucket && fileBucket) {
            return true;
        }
        return false;
    }

    /**
     * 分片写入Redis
     * 判断切片是否已存在，如果未存在，则创建基础信息，并保存。
     *
     * @param chunkDTO
     */
    private synchronized long saveToRedis(FileChunkDTO chunkDTO) {
        Set<Integer> uploaded = redisService.getCacheMapValue(chunkDTO.getIdentifier(), "uploaded");
        if (uploaded == null) {
            uploaded = new HashSet<>(Arrays.asList(chunkDTO.getChunkNumber()));
            HashMap<String, Object> objectObjectHashMap = new HashMap<>();
            objectObjectHashMap.put("uploaded", uploaded);
            objectObjectHashMap.put("totalChunks", chunkDTO.getTotalChunks());
            objectObjectHashMap.put("totalSize", chunkDTO.getTotalSize());
//            objectObjectHashMap.put("path", getFileRelativelyPath(chunkDTO.getIdentifier(), chunkDTO.getFilename()));
            objectObjectHashMap.put("path", chunkDTO.getFilename());
            redisService.setCacheMap(chunkDTO.getIdentifier(), objectObjectHashMap);
        } else {
            uploaded.add(chunkDTO.getChunkNumber());
            redisService.setCacheMapValue(chunkDTO.getIdentifier(), "uploaded", uploaded);
        }
        return uploaded.size();
    }

}
